Skip to content

Tests for the shared-pass owner map; serialization evidence for the property-order texture-name fallback - #2

Merged
StArraySharp merged 6 commits into
StArraySharp:mainfrom
HitMargin:main
Nov 1, 2026
Merged

StArraySharp merged 6 commits into
StArraySharp:mainfrom
HitMargin:main

Conversation

@2228293026

Copy link
Copy Markdown
Contributor

What

Three unit tests lock in the shared-blob behavior from #50b3105, one unused-field fix, and a comment addition recording serialization evidence for the property-order texture-name fallback.

Shared-pass owner map tests

PassMaps is now internal (InternalsVisibleTo already covers the test assembly) and three tests lock the behavior in:

  • a blob slot shared by two passes lists both owners (the pre-#50b3105 single-owner map silently dropped the program from every other pass);
  • distinct slots keep their own passes;
  • on a shared slot the sub-program with the richer reflection wins.

Serialization evidence for the property-order fallback

While evaluating this build against a DLC corpus (ADOFAI, Unity 6000.3.25f1) we probed the serialized shader the fallback comment's 849/849 measurement was missing a mechanism for. ADOFAI/Frames (a sprite-pipeline shader):

  • carries zero m_TextureParams on all eight sub-programs (both m_Parameters and m_CommonParameters), and
  • its pass m_NameIndices table lists no texture at all -- only cbuffers, engine matrices and the color/flip/frame scalars.

So for the sprite pipeline the renderer owns t0 by convention and property order is genuinely the only signal in the asset; the inference there is not just statistically right but structurally forced. The comment now records this, plus the failure mode to watch for: a game that only ever SetTexture()s a later property writes a slot nothing samples, and the leading name then mislabels the sprite (measured in the same game -- a _PlanetTex property no compiled program ever reads).

Verification

dotnet test src/ShaderRipper.Tests -c Release: 147/147 (144 existing + 3 new). The DLC corpus (425 shaders) re-rips byte-identical on the non-annotation lines with this build.

WarnIfForeign keys on the first 8KB containing the string ShaderRipper
to tell the tool's own output from a user's file, but the emitter no
longer writes that header -- so re-running into the same directory
warned on 419 of 425 files, every one of them written by the previous
run. Emitting the header again restores the invariant: same corpus,
same directory, re-run warnings drop to zero.
…name mechanism

- PassMaps is now internal (InternalsVisibleTo already covers the test
  assembly) and three unit tests lock the shared-blob behavior in: a slot
  lists every owner pass, distinct slots keep their own passes, and the
  sub-program with the richer reflection wins a shared slot.
- CbResolver: record the serialization evidence behind the property-order
  fallback.  A sprite-pipeline shader in the wild (ADOFAI/Frames,
  Unity 6000.3.25f1) carries zero m_TextureParams on all eight
  sub-programs and its pass name table lists no texture at all -- the
  sprite renderer owns t0 by convention, so property order is the only
  source there is.  Also notes the failure mode: a host that only ever
  SetTexture()s a later property writes a slot nothing samples.
- BundleTests: drop the never-assigned CompressInfo field (CS0649).
UnityPlayer's sprite path (SpriteIntermediateRenderer::SetupProperties)
resolves the main texture by NAME -- [MainTexture] attribute first, then
the literal _MainTex (kSLPropMainTex), then the first texture property --
and hands that name to the property sheet; t0 is the renderer's slot by
convention, not a lookup.  ADOFAI/Frames proves the serialization side:
all eight sub-programs carry zero m_TextureParams and the pass name
table lists no texture, yet t0 is the sprite.

So when m_TextureParams is empty and the pass name table carries
UnityPerDrawSprite (the sprite-pipeline marker), name the lowest open
register with the [MainTexture]-flagged property or the literal
_MainTex before falling back to declaration order.  Everything past the
main texture has no engine mechanism and stays a declaration-order
guess -- the file note now says which registers came from where.

Suggested by engine-side analysis of UnityPlayer.dll (three-level
lookup + SetupProperties call site).
@2228293026

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit refining the texture-name fallback's mechanism: the engine resolves the main texture by NAME (three levels: [MainTexture] attribute → literal _MainTex → first texture property; sprite path = SpriteIntermediateRenderer::SetupProperties), not by declaration order -- so the fallback now names t0 via the engine's main-texture name (gated on the UnityPerDrawSprite pass-table marker) before property order, and the file note reports per-register provenance. Credits go to an engine-side analysis of UnityPlayer.dll that corrected my earlier 'property order is the only signal' attribution.

@StArraySharp

Copy link
Copy Markdown
Owner

GLSL evidence: reproducible source, and a platform-slot correction

You asked for a reproducible file and command. Here it is. The short version: the GLSL is not in the Windows depot, and it is not in the gles3 slot. It is in the Linux depot, in the openglcore (platform id 15) slot.

Input

D:\Softwares\Steam\steamapps\content\app_977950\depot_977954
    \ADanceOfFireAndIce_Data\resources.assets
property value
size 1,071,075,592 bytes
last write 2026-10-17 17:44:25
MD5 DC1DE7E65C8B9CB0EDADB0B696AAF505

Command

.\ShaderRipper.Application.exe `
  --file "<linux depot>\ADanceOfFireAndIce_Data\resources.assets" `
  <outdir> --programs --orig-asm --only "ADOFAI/Frames" --platform openglcore --overwrite

Result: 文件=1 着色器=1 程序=4 已翻译=4 失败=0, producing 4_glsl.orig.glsl, 5_glsl.orig.glsl, 6_glsl.orig.glsl, 7_glsl.orig.glsl.

Per-platform slot probe (same command, only --platform changes)

--platform Linux depot Windows depot
d3d11 programs = 0 programs = 8
gles3 programs = 0 programs = 0
openglcore programs = 4 programs = 0
gles20 programs = 0 programs = 0
d3d9 programs = 0 programs = 0

Two independent things are being crossed here, and I think that is where the 425-file measurement went wrong:

  1. Which depot. The Windows build ships D3D11 only. I verified this myself and had to withdraw an earlier claim that Windows had GLSL — it does not. gles3 / openglcore / gles20 on the Windows depot all yield 程序=0, and the emitted .shader files say // This pass has no program in the build.
  2. Which slot. Even in the Linux depot, --platform gles3 yields 程序=0. Only openglcore (15) carries the payload. So a sweep that uses gles3 returns zero UNITY_LOCATION hits regardless of whether GLSL exists, which means that measurement cannot distinguish "this build has no GLSL" from "wrong slot index".

The attachment itself

7_glsl.orig.glsl — FRAGMENT stage, ETC1_EXTERNAL_ALPHA + PIXELSNAP_ON variant. 5_glsl.orig.glsl is the no-alpha variant. Both are inlined at the end of this comment, together with the other two variants.

The load-bearing lines (7_glsl.orig.glsl, fragment):

#define UNITY_LOCATION(x) layout(location = x)
...
uniform 	float _EnableExternalAlpha;
uniform 	float _Frame;
uniform 	float _Frames;
UNITY_LOCATION(0) uniform  sampler2D _PlanetTex;
UNITY_LOCATION(1) uniform  sampler2D _AlphaTex;
...
    u_xlat1 = texture(_AlphaTex, u_xlat0.xy);
    u_xlat0 = texture(_PlanetTex, u_xlat0.xy);

layout(location = 0) on a sampler2D is the GL sampler binding, i.e. the t0 slot. It says _PlanetTex. _MainTex, _AlphaMask are absent — the compiler dropped them because nothing samples them.

Why I still think this matters

Your double-bind finding (_MainTex and _PlanetTex both referencing planet-gold.png, same guid) is a good observation and I accept it: at render time the two namings are indistinguishable, and the exported file is correct either way. That is a solid argument for keeping inferred, and your "the tool must not pretend authority" principle is right.

But two things are different:

  • Render equivalence is not compiler-assignment evidence. It shows either name works, not which name the compiler actually emitted. Where a direct artifact exists (layout(location = 0)), that artifact should win over inference rather than being overridden by it.
  • The new [MainTexture] / literal _MainTex branch does override it. In 8f8c236, when UnityPerDrawSprite is in the pass name table, the lowest open register is force-named with the [MainTexture]-flagged property, else the literal _MainTex, and the file note labels it engine main-texture name, sprite pipeline — which reads as authoritative. For ADOFAI/Frames that would print t0=_MainTex (engine main-texture name, sprite pipeline) while the Linux artifact says location 0 is _PlanetTex. That is the one case where the note is less honest than the old inferred from property order, because it claims a source that the corpus contradicts.

Suggestion, if you want to keep the branch: consult the GLSL slot first when it is present, gate the name-based fallback behind "no GLSL slot for this platform", and keep the note on the same inferred footing instead of calling it the engine's answer. Note the GLSL slot is only obtainable from the Linux depot, so for a Windows-only asset the fallback still needs an honest inferred label.

I also owe you a correction on the mechanism attribution: my earlier claim leaned on SpriteIntermediateRenderer::SetupProperties, which is Unity's internal sprite batcher, not the game shader's own binding path. You were right to call that out. The slot table above does not depend on that argument — it is just the artifact.


Attachment: 4_glsl.orig.glsl (fragment, no alpha, no pixelsnap)
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#ifdef GL_ARB_shader_bit_encoding
#extension GL_ARB_shader_bit_encoding : enable
#endif

#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
#define UNITY_UNIFORM
#else
#define UNITY_UNIFORM uniform
#endif
#define UNITY_SUPPORTS_UNIFORM_LOCATION 1
#if UNITY_SUPPORTS_UNIFORM_LOCATION
#define UNITY_LOCATION(x) layout(location = x)
#define UNITY_BINDING(x) layout(binding = x, std140)
#else
#define UNITY_LOCATION(x)
#define UNITY_BINDING(x) layout(std140)
#endif
uniform 	float _Frame;
uniform 	float _Frames;
UNITY_LOCATION(0) uniform  sampler2D _PlanetTex;
in  vec4 vs_COLOR0;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_Target0;
vec4 u_xlat0;
float u_xlat1;
bool u_xlatb1;
void main()
{
    u_xlat0.x = floor(_Frame);
    u_xlat0.x = u_xlat0.x + vs_TEXCOORD0.x;
    u_xlat0.x = u_xlat0.x / _Frames;
    u_xlatb1 = u_xlat0.x>=(-u_xlat0.x);
    u_xlat1 = (u_xlatb1) ? 1.0 : -1.0;
    u_xlat0.x = u_xlat1 * u_xlat0.x;
    u_xlat0.x = fract(u_xlat0.x);
    u_xlat0.x = u_xlat0.x * u_xlat1;
    u_xlat0.y = vs_TEXCOORD0.y;
    u_xlat0 = texture(_PlanetTex, u_xlat0.xy);
    u_xlat0 = u_xlat0 * vs_COLOR0;
    SV_Target0.xyz = u_xlat0.www * u_xlat0.xyz;
    SV_Target0.w = u_xlat0.w;
    return;
}

#endif
Attachment: 5_glsl.orig.glsl (fragment, no alpha)
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#ifdef GL_ARB_shader_bit_encoding
#extension GL_ARB_shader_bit_encoding : enable
#endif

#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
#define UNITY_UNIFORM
#else
#define UNITY_UNIFORM uniform
#endif
#define UNITY_SUPPORTS_UNIFORM_LOCATION 1
#if UNITY_SUPPORTS_UNIFORM_LOCATION
#define UNITY_LOCATION(x) layout(location = x)
#define UNITY_BINDING(x) layout(binding = x, std140)
#else
#define UNITY_LOCATION(x)
#define UNITY_BINDING(x) layout(std140)
#endif
uniform 	float _Frame;
uniform 	float _Frames;
UNITY_LOCATION(0) uniform  sampler2D _PlanetTex;
in  vec4 vs_COLOR0;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_Target0;
vec4 u_xlat0;
float u_xlat1;
bool u_xlatb1;
void main()
{
    u_xlat0.x = floor(_Frame);
    u_xlat0.x = u_xlat0.x + vs_TEXCOORD0.x;
    u_xlat0.x = u_xlat0.x / _Frames;
    u_xlatb1 = u_xlat0.x>=(-u_xlat0.x);
    u_xlat1 = (u_xlatb1) ? 1.0 : -1.0;
    u_xlat0.x = u_xlat1 * u_xlat0.x;
    u_xlat0.x = fract(u_xlat0.x);
    u_xlat0.x = u_xlat0.x * u_xlat1;
    u_xlat0.y = vs_TEXCOORD0.y;
    u_xlat0 = texture(_PlanetTex, u_xlat0.xy);
    u_xlat0 = u_xlat0 * vs_COLOR0;
    SV_Target0.xyz = u_xlat0.www * u_xlat0.xyz;
    SV_Target0.w = u_xlat0.w;
    return;
}

#endif
Attachment: 6_glsl.orig.glsl (fragment, ETC1_EXTERNAL_ALPHA)
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#ifdef GL_ARB_shader_bit_encoding
#extension GL_ARB_shader_bit_encoding : enable
#endif

#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
#define UNITY_UNIFORM
#else
#define UNITY_UNIFORM uniform
#endif
#define UNITY_SUPPORTS_UNIFORM_LOCATION 1
#if UNITY_SUPPORTS_UNIFORM_LOCATION
#define UNITY_LOCATION(x) layout(location = x)
#define UNITY_BINDING(x) layout(binding = x, std140)
#else
#define UNITY_LOCATION(x)
#define UNITY_BINDING(x) layout(std140)
#endif
uniform 	float _EnableExternalAlpha;
uniform 	float _Frame;
uniform 	float _Frames;
UNITY_LOCATION(0) uniform  sampler2D _PlanetTex;
UNITY_LOCATION(1) uniform  sampler2D _AlphaTex;
in  vec4 vs_COLOR0;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_Target0;
vec4 u_xlat0;
vec4 u_xlat1;
float u_xlat2;
bool u_xlatb2;
void main()
{
    u_xlat0.x = floor(_Frame);
    u_xlat0.x = u_xlat0.x + vs_TEXCOORD0.x;
    u_xlat0.x = u_xlat0.x / _Frames;
    u_xlatb2 = u_xlat0.x>=(-u_xlat0.x);
    u_xlat2 = (u_xlatb2) ? 1.0 : -1.0;
    u_xlat0.x = u_xlat2 * u_xlat0.x;
    u_xlat0.x = fract(u_xlat0.x);
    u_xlat0.x = u_xlat0.x * u_xlat2;
    u_xlat0.y = vs_TEXCOORD0.y;
    u_xlat1 = texture(_AlphaTex, u_xlat0.xy);
    u_xlat0 = texture(_PlanetTex, u_xlat0.xy);
    u_xlat1.x = (-u_xlat0.w) + u_xlat1.x;
    u_xlat0.w = _EnableExternalAlpha * u_xlat1.x + u_xlat0.w;
    u_xlat0 = u_xlat0 * vs_COLOR0;
    SV_Target0.xyz = u_xlat0.www * u_xlat0.xyz;
    SV_Target0.w = u_xlat0.w;
    return;
}

#endif
Attachment: 7_glsl.orig.glsl (fragment, ETC1_EXTERNAL_ALPHA + PIXELSNAP_ON)
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#ifdef GL_ARB_shader_bit_encoding
#extension GL_ARB_shader_bit_encoding : enable
#endif

#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
#define UNITY_UNIFORM
#else
#define UNITY_UNIFORM uniform
#endif
#define UNITY_SUPPORTS_UNIFORM_LOCATION 1
#if UNITY_SUPPORTS_UNIFORM_LOCATION
#define UNITY_LOCATION(x) layout(location = x)
#define UNITY_BINDING(x) layout(binding = x, std140)
#else
#define UNITY_LOCATION(x)
#define UNITY_BINDING(x) layout(std140)
#endif
uniform 	float _EnableExternalAlpha;
uniform 	float _Frame;
uniform 	float _Frames;
UNITY_LOCATION(0) uniform  sampler2D _PlanetTex;
UNITY_LOCATION(1) uniform  sampler2D _AlphaTex;
in  vec4 vs_COLOR0;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_Target0;
vec4 u_xlat0;
vec4 u_xlat1;
float u_xlat2;
bool u_xlatb2;
void main()
{
    u_xlat0.x = floor(_Frame);
    u_xlat0.x = u_xlat0.x + vs_TEXCOORD0.x;
    u_xlat0.x = u_xlat0.x / _Frames;
    u_xlatb2 = u_xlat0.x>=(-u_xlat0.x);
    u_xlat2 = (u_xlatb2) ? 1.0 : -1.0;
    u_xlat0.x = u_xlat2 * u_xlat0.x;
    u_xlat0.x = fract(u_xlat0.x);
    u_xlat0.x = u_xlat0.x * u_xlat2;
    u_xlat0.y = vs_TEXCOORD0.y;
    u_xlat1 = texture(_AlphaTex, u_xlat0.xy);
    u_xlat0 = texture(_PlanetTex, u_xlat0.xy);
    u_xlat1.x = (-u_xlat0.w) + u_xlat1.x;
    u_xlat0.w = _EnableExternalAlpha * u_xlat1.x + u_xlat0.w;
    u_xlat0 = u_xlat0 * vs_COLOR0;
    SV_Target0.xyz = u_xlat0.www * u_xlat0.xyz;
    SV_Target0.w = u_xlat0.w;
    return;
}

#endif
Cross-check: ADOFAI/MaskedSprite (property order does match slots here)

For contrast, the same probe on the Linux depot for ADOFAI/MaskedSprite:

; 4_glsl / 5_glsl (no external alpha)
UNITY_LOCATION(0) uniform  sampler2D _MainTex;
UNITY_LOCATION(1) uniform  sampler2D _MaskTex;

; 6_glsl / 7_glsl (ETC1_EXTERNAL_ALPHA)
UNITY_LOCATION(0) uniform  sampler2D _MainTex;
UNITY_LOCATION(1) uniform  sampler2D _AlphaTex;
UNITY_LOCATION(2) uniform  sampler2D _MaskTex;

Property declaration order is _MainTex, _Color, PixelSnap, _RendererColor, _Flip, _AlphaTex, _EnableExternalAlpha, _MaskTex — identical to the slot order. So declaration order is a good heuristic when every declared texture is actually sampled; Frames is the case where three declared textures are dead and the only live one is declared last.

… _MainTex

The Linux depot's openglcore slot is the name authority this tool was
missing: the GLSL text keeps UNITY_LOCATION(n) uniform sampler2D <name>
where the DXBC payload strips reflection and the serialized parameter
tables are empty.  ADOFAI/Frames: location 0 is _PlanetTex, location 1
is _AlphaTex -- the DXBC t0/t1 of the same source.

GameExport now pre-scans the shader's GL-family slots (per stage),
extracts the sampler location->name pairs, and threads them into the
DXBC resolver; MapTextures pairs locations with texture registers
positionally and reports the provenance in the file note
("texture names from the shader's own GLSL source: t0=_PlanetTex, ...").

The [MainTexture]/_MainTex literal branch from 8f8c236 now runs only
when no GLSL source exists for the shader, and its note is on the same
inferred footing as property order ("main-texture convention,
unverified: the asset keeps no binding") instead of claiming the
engine's answer.  The SetupProperties mechanism attribution is
withdrawn -- the GLSL artifact is the evidence, not the engine path.

147/147 tests pass; the Windows corpus re-rip is unchanged except the
reworded note.
@2228293026

Copy link
Copy Markdown
Contributor Author

Accepted -- the fix is pushed as a follow-up commit on the PR branch (eddc950, supersedes 8f8c236). What changed:

  1. GLSL slots are now the name authority. GameExport pre-scans the shader's GL-family platforms (openglcore included -- thank you for the depot_977954 pointer, the depot table on SteamDB renders truncated and I wrongly concluded no GLSL existed), extracts the sampler location→name pairs per stage from the GLSL source, and threads them into the DXBC resolver. MapTextures pairs locations with texture registers positionally and the file note reports the provenance: texture names from the shader's own GLSL source: t0=_PlanetTex, t1=_AlphaTex.

  2. The _MainTex literal branch is demoted. It only runs when the shader has no GL-family slot at all, and its note is on the same inferred footing as property order: t0=_MainTex (main-texture convention, unverified: the asset keeps no binding). The 'engine main-texture name' phrasing is gone, and the SetupProperties mechanism attribution is withdrawn -- you were right that it is the internal batcher's path and cannot vouch for the game shader.

  3. 147/147 tests pass; the Windows-only corpus re-rip is unchanged except the reworded note.

One verification request: on your machine, the same command against depot_977954 with this build should now print texture names from the shader's own GLSL source: t0=_PlanetTex, t1=_AlphaTex in the file header notes -- that would close the loop end to end.

@StArraySharp

Copy link
Copy Markdown
Owner

✅ Confirmed on depot_977954 (resources.assets, MD5 DC1DE7E65C8B9CB0EDADB0B696AAF505): ADOFAI_Frames now emits Texture2D _PlanetTex; / _AlphaTex, and Windows-only fallback prints t0=_MainTex (main-texture convention, unverified...). Both as specified.

But the GLSL slot scan looks shader-global rather than per-SubShader: ADOFAI/FloorMesh has two SubShaders whose GLSL disagrees —

SubShader 0: location 0=_MainTex, 1=_PerlinTex, 2=_TileTex, 3=_IconTex, 4=_IconOutlineTex
SubShader 1 (the FloorMeshLevelSelect variant): location 0=_TileTex only
The DXBC export names all three passes t0=_MainTex ... t4=_IconOutlineTex, so the second SubShader's t0=_TileTex got overwritten by the first's table. Worth scoping the location→name map per SubShader (or per pass) before this ships.

@StArraySharp
StArraySharp merged commit 6ea7285 into StArraySharp:main Nov 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants